第二章 编程惯用法

建议 8:利用 assert 语句来发现问题

断言(assert)在很多语言中都存在,它主要为调试程序服务,能够快速方便地检查程序的异常或者发现不恰当的输入等,可防止意想不到的情况出现。 其基本语法如下: assert expression1, ["," expression2],使用的例子如下:

x, y = 1, 2
assert x == y, "not equal"

在执行过程中它实际相当于如下代码:

x, y = 1, 2
if __debug__ and not x == y:
    raise AssertionError("not equals")

对 Python 中使用断言需要说明如下:

  • __debug__ 的值默认设置为 True,且是只读的,在 Python 2.7 中还无法修改该值。
  • 断言是有代价的,它会对性能产生一定的影响,对于编译型的语言这也许并不那么重要,因为断言只在调试模式下启用。但 Python 并没有严格定义调试和发布模式之间的区别,通常禁用断言的方法是在运行脚本的时候加上 -O 标志,这种方式带来的影响是它并不优化字节码,而是忽略与断言相关的语句。

断言实际是被设计用来捕获用户所定义的约束的,而不是用来捕获程序本身错误的,因此使用断言需要注意以下几点:

  • 不要滥用。若由于断言引发了异常,通常代表程序中存在 bug。因此断言应该使用在正常逻辑不可到达的地方或正常情况下总是为真的场合。
  • 如果 Python 本身的异常能够处理就不要再使用断言。如对于类似于数组越界、类型不匹配、除数为 0 之类的错误,不建议使用断言来进行处理。
# 下面的断言是多余的, 如果类型不匹配本身就会抛出异常
def stradd(x, y):
    assert isinstance(x, basestring)
    assert isinstance(y, basestring)
    return x + y
  • 不要使用断言来检查用户的输入。如对于一个数字类型,如果根据用户的设计该值的范围应该是 2~10,较好的做法是使用条件判断,并在不符合条件的时候输出错误提示信息。
  • 在函数调用后,当需要确认返回值是否合理时可以使用断言
  • 当条件是业务逻辑继续下去的先决条件时可以使用断言,比如 list1 和其副本 list2,如果由于某些不可控的因素,如使用了浅拷贝而 list1 中含有可变对象等,就可以使用断言来判断这两者的关系。

建议 9:数据交换值的时候不推荐使用中间变量

建议 10:充分利用 Lazy evaluation 的特性

  • 避免不必要的计算 比如对于 if x and y: 把小概率的语句放在前面可以节省一定的性能
  • 节省空间,利用 迭代器进行惰性计算

建议 11:理解枚举替代实现的缺陷

Python 没有内置枚举类型时的替代选择

  • 使用类属性
class Seasons:
    Spring, Summer, Autumn, Winter = 0, 1, 2, 3
# 或者可以简化为:
class Seasons:
    Spring, Summer, Autumn, Winter = range(4)
  • 借助函数
def enum(*posarg, **keysarg):
    return type("Enum", (object,), dict(zip(posarg, range(len(posarg))), **keysarg))
Seasons = enum("Spring", "Summer", "Autumn", Winter=1)
  • 使用 collections.namedtuple
Seasons = namedtuple("Seasons", "Spring Summer Autumn Winter")._make(range(4))

以上几种替代实现都有一些不合理的地方:

  • 允许枚举值重复,比如 collections.namedtuple
  • 支持无意义操作,比如 Seasons.Summer + Seasons.Autumn == Season.Winter

还有另一种实现,第三方模块 flufl.enum 包含两种枚举类,Enum 只保证枚举值唯一,IntEnum 还保证枚举值为 int

建议 12:不推荐使用 type 来进行类型检查

内建函数 type(object) 用于返回当前对象的类型,因此可以通过与 Python 自带模块 types 中所定义的名称进行比较,根据其返回值确定变量类型是否符合要求。所有基本类型对应的名称都可以在 types 模块中找到,然而使用 type() 函数并不适合用来 进行变量类型检查。

  • 基于内建类型扩展的用户自定义类型,type 函数并不能准确返回结果
  • 在古典类中,所有类的实例的 type 值都相等

对于内建的基本类型来说,使用 type() 进行类型检查问题不大,但在某些特殊场合 type() 方法并不可靠。解决方法是,如果类型有对应的工厂函数,可以使用工厂函数对类型 做相应转换,否则可以使用 isinstance() 函数来检测。

isinstance 会考虑子类和继承,而 type 并不考虑这些情况

建议 13:尽量转换为浮点类型后再做除法

这是 Python2 中的问题

建议 14:警惕 eval() 的安全漏洞

Python 中 eval() 函数将字符串 str 当成有效的表达式来求值并返回计算结果。 eval(expression[, globals[, locals]])

参数 globals 为字典形式,locals 为任何映射对象,它们分别表示全局和局部命名空间。如果传入 globals 参数的字典中缺少 __builtins__ 的时候,当前的全局命名空间将作为 globals 参数输入并且在表达式计算之前被解析。locals 参数默认与 globals 相同,如果两者都省略的话,表达式将在 eval() 调用的环境中执行。

eval 存在安全漏洞,一个简单的例子

import sys
from math import *
def ExpCalcBot(string):
try:
print "Your answer is", eval(user_func) # 计算输入的值
except NameError:
print "The expression you enter is not valid"
print 'Hi, I am ExpCalcBot. please input your expression or enter e to end'
inputstr = ''
while True:
print 'Please enter a number or operation. Enter c to complete. :'
inputstr = raw_input()
if inputstr == str('e'): # 遇到输入为 e 的时候退出
sys.exit()
elif repr(inputstr) != repr(''):
ExpCalcBot(inputstr)
inputstr = ''

由于网络环境下运行它的用户并非都是可信任的,比如输入 __import__("os").system("dir"),它会显示当前目录下的所有文件列表;输入 __import__("os").system("del * /Q") ,会导致当前目录下的所有文件都被删除了,而这一切没有任何提示。

在 globals 参数中禁止全局命名空间的访问

def ExpCalcBot(string):
    try:
        math_fun_list = ["acos", "asin", "atan", "cos", "e", "log", "log10", "pi", "pow", "sin", "sqrt", "tan"]
        math_fun_dict = dict([(k, globals().get(k)) for k in math_fun_list]) # 形成可以访问的函数的字典
        print "Your name is", eval(string, {"__builtins__": None}, math_fun_dict)
    except NameError:
        print "The expression you enter is not valid"

再次进行恶意输入: [c for c in ().__class__.__bases__[0].__subclasses__() if c.__name__== "Quitter"][0](0)(),其中 ().__class__.__bases__[0].__subclasses__() 用来显示 object 类的所有子类。类 Quitter 与 "quit" 功能绑定,因此上面的输入会导致程序退出。

应该避免使用 eval,在需要使用 eval 的地方可用安全性更好的 ast.literal_eval 替代。

建议 15:使用 enumerate() 获取序列迭代的索引和值

很多种实现方法:

# 方法一:在每次循环中对索引变量进行自增
li = ['a', 'b', 'c', 'd']
index = 0
for i in li:
print "index:", index, "element:", i
index += 1
# 方法二:使用 range() 和 len() 方法结合
li = ['a', 'b', 'c', 'd', 'e']
for i in range(len(li)):
print "index:", i, "element:", li[i]
# 方法三:使用 while 循环,用 len() 获取循环次数
li = ['a', 'b', 'c', 'd', 'e']
index = 0
while index < len(li):
print "index:", index, "element:", li[index]
index += 1
# 方法四:使用 zip() 方法
li = ['a', 'b', 'c', 'd', 'e']
for i, e in zip(range(len(li)), li):
print "index:", i, "element:", e
# 方法五:使用 enumerate() 获取序列迭代的索引和值
li = ['a', 'b', 'c', 'd', 'e']
for i, e in enumerate(li):
print "index:", i, "element:", e

不过推荐使用 enumerate

建议 16:分清 == 与 is 的适用场景

is 判断是否同一块内存,== 调用 __eq__ 方法

对于小整数和字符串,会存在驻留机制。